Swadhin Biswas Feature/ai code reviewer (#79) · 8/16/2026
cfd400d
100 lines · 3.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import type { APIRoute } from "astro";
import { getDatabase, schema } from "@/db";
import { eq, and } from "drizzle-orm";
import { getUserFromRequest } from "@/lib/auth";
import { unauthorized, badRequest, success, notFound, forbidden } from "@/lib/api";
import { withErrorHandler } from "@/lib/errors";
import { logger } from "@/lib/logger";
import type { NodePgDatabase } from "drizzle-orm/node-postgres";
import { canAdminRepo, getOrgPermission } from "@/lib/permissions";

/**
 * WS3-05: transfer a repository to an organization.
 * POST /api/repos/{owner}/{repo}/transfer  { orgName: string }
 * Only the repo admin (or site admin) can transfer; the caller must be
 * an owner/admin of the target org.
 */
export const POST: APIRoute = withErrorHandler(async ({ request, params }) => {
  const user = await getUserFromRequest(request);
  if (!user) return unauthorized();

  const { owner, repo } = params;
  if (!owner || !repo) return badRequest("Missing parameters");

  const body = await request.json().catch(() => null);
  const orgName = body?.orgName;
  if (!orgName) return badRequest("orgName is required");

  const db = getDatabase() as NodePgDatabase<typeof schema>;

  // Resolve the repo
  const ownerUser = await db.query.users.findFirst({
    where: eq(schema.users.username, owner),
  });
  if (!ownerUser) return notFound("Repository not found");
  const repository = await db.query.repositories.findFirst({
    where: and(
      eq(schema.repositories.ownerId, ownerUser.id),
      eq(schema.repositories.name, repo),
    ),
  });
  if (!repository) return notFound("Repository not found");

  // Only admins of the repo can transfer
  if (!(await canAdminRepo(user.userId, repository, { isAdmin: user.isAdmin ?? undefined }))) {
    return forbidden("Only repository admins can transfer it");
  }

  // Resolve the target org
  const organization = await db.query.organizations.findFirst({
    where: eq(schema.organizations.name, orgName),
  });
  if (!organization) return notFound("Organization not found");

  // Caller must be owner/admin of the target org
  const orgPermission = await getOrgPermission(user.userId, organization.id, {
    isAdmin: user.isAdmin,
  });
  if (orgPermission !== "owner" && orgPermission !== "admin") {
    return forbidden("You must be an owner or admin of the target organization");
  }

  // Cannot transfer a repo already owned by the org
  if (repository.ownerType === "organization" && repository.ownerId === organization.id) {
    return badRequest("Repository is already owned by this organization");
  }

  // Transfer: repoint owner to the org
  await db
    .update(schema.repositories)
    .set({ ownerId: organization.id, ownerType: "organization", updatedAt: new Date() })
    .where(eq(schema.repositories.id, repository.id));

  // Make the caller the initial admin collaborator on the repo
  const existingCollab = await db.query.repositoryCollaborators.findFirst({
    where: and(
      eq(schema.repositoryCollaborators.repositoryId, repository.id),
      eq(schema.repositoryCollaborators.userId, user.userId),
    ),
  });
  if (!existingCollab) {
    await db.insert(schema.repositoryCollaborators).values({
      id: crypto.randomUUID(),
      repositoryId: repository.id,
      userId: user.userId,
      role: "owner",
      createdAt: new Date(),
    });
  }

  logger.info(
    { userId: user.userId, repoId: repository.id, orgId: organization.id },
    "Repository transferred to organization",
  );

  return success({
    message: "Repository transferred",
    url: `/${orgName}/${repo}`,
  });
});